﻿// HSS.Interlink.Web.BaseUploadHandler.cs
// ----------------------------------------------------------------------------
// Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.html)
// ----------------------------------------------------------------------------
// HighSpeed-Solutions, LLC
// Copyright (c) 2001-2010
// ----------------------------------------------------------------------------
// File:       BaseUploadHandler.cs
// Author:     HSS\gbanta
// Created:    07/16/2011
// Modified:   07/22/2011
// ----------------------------------------------------------------------------
namespace HSS.Interlink.Web
{
	#region Using Directives
	using System;
	using System.Configuration;
	using System.IO;
	using System.Security.Cryptography;
	using System.Web;
	using System.Web.Configuration;
	using System.Net;
	#endregion

	#region BaseUploadHandler
	/// <summary>
	/// BaseUploadHandler is the base implementation of a File upload
	/// processor and handles the Protocol portion of a upload request.
	/// Implementors of this class should override the abstract processing
	/// methods.
	/// </summary>
	public abstract class BaseUploadHandler : BaseHandler, IUploadHandler
	{
		#region Fields
		/// <summary>
		/// Array of hex values 0-F
		/// </summary>
		static int[] hexValue = new int[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
		#endregion

		#region Constructors
		/// <summary>
		/// Constructor
		/// </summary>
		public BaseUploadHandler()
		{

		}
		#endregion

		#region Properties
		/// <summary>
		/// Gets or sets the absolute path to the directory where Interlink stores temporary files.
		/// </summary>
		/// <remarks>
		/// <para>The default path; ~\temp\uploadtemp.</para>
		/// </remarks>
		public string TempFolder
		{
			get
			{
				if (!_tempFolderSet)
				{
					_tempFolderSet = true;
					_tempFolder = InterlinkUtilities.GetAppRelativeTempFolder("uploadtemp");
				}
				return _tempFolder;
			}
			set
			{
				_tempFolderSet = true;
				_tempFolder = value;
			}
		} string _tempFolder; bool _tempFolderSet;
		#endregion

		#region Methods

		/// <summary>
		/// Processes the actual upload request. This method is used by HSS.Interlink and not meant to be called from user code.
		/// </summary>
		/// <param name="context">The current HttpContext</param>
		/// <param name="query">The FileQuery</param>
		void IUploadHandler.Upload(HttpContext context, FileQuery query)
		{
			Responses response = Responses.Success;
			string jobFile = string.Empty;
			try
			{
				if (!this.CommonInit(context, query))
				{
					this.OnError(new WebException("Failed to initliaze the handler."));
					return;
				}

				jobFile = Path.Combine(this.TempFolder, this.Query.JobId.ToString());
				string method = context.Request.HttpMethod.ToUpper();
				switch (method)
				{
					case "POST":
						#region POST commands
						switch (this.Query.Command)
						{
							case FileQueryCommand.Prepare:
								#region Prepare
								context.Request.SaveAs(jobFile, false);
								context.Response.Clear();
								context.Response.Write(this.GetMaxRequestLength());
								return;
								#endregion

							case FileQueryCommand.Append:
								#region Append

								if (!this.SetMetadata(jobFile))
								{
									this.OnError(new Exception("Missing job file."));
									return;
								}
								if (!IsAuthorized(jobFile))
									return;

								// Read and validate the chunk and it's hash
								Stream inputStream = context.Request.InputStream;
								long fileLen = inputStream.Length;
								long hashLen = 32; // (256BIT/8)
								long bufferLen = fileLen - hashLen;
								byte[] buffer = new byte[bufferLen];
								byte[] receivedHash = new byte[hashLen];
								inputStream.Read(buffer, 0, buffer.Length);
								inputStream.Read(receivedHash, 0, receivedHash.Length);
								inputStream.Close();
								byte[] actualHash = new SHA256Managed().ComputeHash(buffer);
								if (!CompareHash(receivedHash, actualHash))
								{
									this.OnError(new Exception("Received FileAppend and content hash does not matach."));
									response = Responses.FatalError;
								}
								else
								{
									try
									{
										response = this.AppendToFile(buffer); // Implementation
									}
									catch (Exception ex)
									{
										this.OnError(ex);
										response = Responses.UnknownException;
									}
								}
								buffer = null;
								receivedHash = null;
								actualHash = null;

								switch (response)
								{
									case Responses.Success:
										context.Response.Clear();
										return;

									case Responses.AppendFileRetry:
										context.Response.Clear();
										context.Response.Write(Response.GetResponse(Responses.AppendFileRetry));
										return;

									default:
										context.Response.Clear();
										context.Response.Write(Response.GetResponse(response));
										return;
								}
								#endregion

							default:
								#region Default is failure
								if (File.Exists(jobFile))
									File.Delete(jobFile);
								context.Request.InputStream.Close();
								context.Response.Clear();
								context.Response.Write(Response.UnknownRequest);
								this.OnError(new Exception("Unknown POST Request."));
								return;
								#endregion

						}
						#endregion

					case "GET":
						#region GET commands
						switch (this.Query.Command)
						{
							case FileQueryCommand.CheckFileExists:
								#region CheckFileExists
								if (!this.SetMetadata(jobFile))
								{
									this.OnError(new Exception("Missing job file."));
									return;
								}
								if (!this.IsAuthorized(jobFile))
									return;
								bool fileExists = this.CheckFileExists();
								context.Request.InputStream.Close();
								context.Response.Clear();
								if (fileExists)
									context.Response.Write(Response.FileAlreadyExists);
								else
									context.Response.Write(Response.FileDoesNotExist);
								return;
								#endregion

							case FileQueryCommand.NewFile:
								#region NewFile
								if (!this.SetMetadata(jobFile))
								{
									this.OnError(new Exception("Missing job file."));
									return;
								}
								if (!this.IsAuthorized(jobFile))
									return;
								response = this.CreateNewFile();
								context.Request.InputStream.Close();
								switch (response)
								{
									case Responses.Success:
										context.Response.Clear();
										return;

									default:
										context.Response.Clear();
										context.Response.Write(Response.GetResponse(response));
										return;
								}
								#endregion

							case FileQueryCommand.CancelUpload:
								#region Cancel
								if (!this.SetMetadata(jobFile))
								{
									this.OnError(new Exception("Missing job file."));
									return;
								}
								if (!this.IsAuthorized(jobFile))
									return;
								this.CancelUpload();
								context.Request.InputStream.Close();
								context.Response.Clear();
								try
								{
									if (File.Exists(jobFile))
										File.Delete(jobFile);
								}
								catch { }
								return;
								#endregion

							case FileQueryCommand.UploadComplete:
								#region Complete
								if (!this.SetMetadata(jobFile))
								{
									this.OnError(new Exception("Missing job file."));
									return;
								}
								if (!this.IsAuthorized(jobFile))
									return;
								var result = this.UploadComplete();
								context.Request.InputStream.Close();
								context.Response.Clear();
								context.Response.Write(result);
								try
								{
									if (File.Exists(jobFile))
										File.Delete(jobFile);
								}
								catch { }
								return;
								#endregion

							default:
								#region Default is failure
								try
								{
									if (File.Exists(jobFile))
										File.Delete(jobFile);
								}
								catch { }
								context.Request.InputStream.Close();
								context.Response.Clear();
								context.Response.Write(Response.UnknownRequest);
								this.OnError(new Exception("Unknown GET Request."));
								return;
								#endregion

						}
						#endregion

					default:
						#region No other VERBS supported
						try
						{
							if (File.Exists(jobFile))
								File.Delete(jobFile);
						}
						catch { }
						context.Request.InputStream.Close();
						context.Response.Clear();
						context.Response.StatusCode = 405; // Method Not Allowed
						this.OnError(new Exception("HTTP method [" + method + "] not allowed."));
						return;
						#endregion
				}
			}
			catch (Exception ex)
			{
				OnError(ex);

				#region Handler Exceptions
				try
				{
					if (File.Exists(jobFile))
						File.Delete(jobFile);
				}
				catch { }
				try
				{
					if (null != context.Request.InputStream)
						context.Request.InputStream.Close();
					context.Response.Clear();
				}
				catch { }
				try
				{
					context.Response.Write(Response.UnknownException);
				}
				catch { }
				#endregion
			}
			finally
			{
				this.PurgeTempFiles();
			}

		}

		/// <summary>
		/// Gets a temporary file name based on the file extension provided by
		/// combining with a Random GUID.
		/// </summary>
		/// <param name="fileExte">The file extension of the temp file.</param>
		/// <returns>The full path to the temp file by combining the <c>UploadTempFolder</c> with the file name.</returns>
		protected virtual string GetTempFile(string fileExte)
		{
			return Path.Combine(this.TempFolder, Guid.NewGuid().ToString() + fileExte);
		}

		#region Private
		private void PurgeTempFiles()
		{
			this.PurgeTempFiles(this.TempFolder);
		}
		private string GetMaxRequestLength()
		{
			string maxLen = "4096";
			try
			{
				HttpRuntimeSection runtime = ConfigurationManager.GetSection(Properties.Resources.Config_HttpRuntimeConfig_Key) as HttpRuntimeSection;
				maxLen = runtime.MaxRequestLength.ToString();
			}
			catch { }
			return Properties.Resources.Cookie_MaxChunkSize + maxLen;
		}
		#endregion

		#region Abstract
		/// <summary>
		/// Determines if the requested file already exists in the destination
		/// </summary>
		/// <returns>true if the file exists; otherwise false</returns>
		public abstract bool CheckFileExists();
		/// <summary>
		/// This command is called to create a new file, but does not contain
		/// any contents of the file (see AppendToFile).
		/// </summary>
		/// <returns>true if the process was successful; otherwise false.</returns>
		public abstract Responses CreateNewFile();
		/// <summary>
		/// Receives a chunk of data to append to an existing file.
		/// </summary>
		/// <param name="buffer">The bytes available for writing.</param>
		/// <returns>true if the process was successful; otherwise false.</returns>
		public abstract Responses AppendToFile(byte[] buffer);
		/// <summary>
		/// Cancel the current Upload request.
		/// </summary>
		public abstract void CancelUpload();
		/// <summary>
		/// Complete the current Upload request.
		/// </summary>
		/// <returns>An arbitrary string to return as a result of the file being successfully uploaded.</returns>
		public abstract string UploadComplete();
		/// <summary>
		/// Called when an error is encountered.
		/// </summary>
		/// <param name="ex">The exception that was encountered.</param>
		public abstract void OnError(Exception ex);
		#endregion

		#endregion
	}
	#endregion
}